package com.itzg.vboxfarm.service;

import static com.itzg.vboxfarm.bits.Constants.MSEC_PER_SEC;

import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.Random;
import java.util.concurrent.ScheduledFuture;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Scope;
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
import org.springframework.stereotype.Component;
import org.virtualbox_4_1.Holder;
import org.virtualbox_4_1.IPerformanceCollector;
import org.virtualbox_4_1.IUnknown;
import org.virtualbox_4_1.IVirtualBox;
import org.virtualbox_4_1.VBoxException;

import com.itzg.vboxfarm.domain.HostMachine;
import com.itzg.vboxfarm.domain.HostMachineMeasure;

@Component
@Scope("prototype") // also set in bits.xml
public class HostStatsCollector {
	private static Logger logger = LoggerFactory.getLogger(HostStatsCollector.class);

	@Autowired
	ThreadPoolTaskScheduler scheduler;
	
	@Autowired
	VirtualBoxManagerService vboxService;
	
	@Autowired
	HostStorageService hostStorageService;
	
	private static final List<String> METRIC_NAMES_SETUP = Collections.singletonList("CPU/Load,RAM/Usage");
	
	int maxInitialWobble = 10;
	
	long collectionCount = 4;
	
	long collectionPeriod = 5;
	
	long collectionLag = 2;
	
	private HostMachine host;
	
	private Runnable runner = new Runnable() {
		@Override
		public void run() {
			process();
		}
	};
	
	@SuppressWarnings("rawtypes")
	private ScheduledFuture scheduled;

	long lastSetup;

	private boolean needsSetup;

	public HostMachine getHost() {
		return host;
	}
	
	/* (non-Javadoc)
	 * @see com.itzg.vboxfarm.service.HostStatsCollector#prepare(com.itzg.vboxfarm.domain.HostMachine)
	 */
	public void prepare(HostMachine host) {
		this.host = host;
		logger.debug("Preparing for collection from {}", host);
		long myWobble = new Random().nextInt(maxInitialWobble);
		logger.debug("Max wobble is {}, mine is {}", maxInitialWobble, myWobble);
		needsSetup = true;
		scheduled = scheduler.schedule(runner, new Date(System.currentTimeMillis()+myWobble*MSEC_PER_SEC));
	}
	
	/* (non-Javadoc)
	 * @see com.itzg.vboxfarm.service.HostStatsCollector#stop()
	 */
	public void stop() {
		if (scheduled != null) {
			scheduled.cancel(false);
		}
	}

	protected void process() {
		IVirtualBox vbox;

		try {
			vbox = vboxService.createConnection(host);
		} catch (VBoxException e) {
			logger.error("Unable to connect to host for collection", e);
			return;
		}
			
		IPerformanceCollector performanceCollector = vbox.getPerformanceCollector();
		
		try {
			List<String> metricNames = new ArrayList<String>();
			// Weird, it wants literally a comma separated list
			metricNames.add("CPU/Load/User,CPU/Load/User:*,RAM/Usage/Used,RAM/Usage/Free");
			List<IUnknown> objects = new ArrayList<IUnknown>();
			objects.add(vbox.getHost());
			
			if (!needsSetup) {
				
				Holder<List<String>> returnMeasureNames = new Holder<List<String>>();
				Holder<List<IUnknown>> returnObjects = new Holder<List<IUnknown>>();
				Holder<List<String>> returnUnits = new Holder<List<String>>();
				Holder<List<Long>> returnScales = new Holder<List<Long>>();
				Holder<List<Long>> returnSequenceNumbers = new Holder<List<Long>>();
				Holder<List<Long>> returnDataIndices = new Holder<List<Long>>();
				Holder<List<Long>> returnDataLengths = new Holder<List<Long>>();
				List<Integer> values = performanceCollector.queryMetricsData(metricNames, objects, 
						returnMeasureNames, returnObjects, returnUnits, returnScales, returnSequenceNumbers, returnDataIndices, returnDataLengths);
				
				long elapsed = System.currentTimeMillis() - lastSetup;
				logger.debug("Actual time elapsed {} sec", elapsed/MSEC_PER_SEC);
				logger.debug("returnMetricNames {}", returnMeasureNames.value);
				logger.debug("returnObjects {}", returnObjects.value);
				logger.debug("returnUnits {}", returnUnits.value);
				logger.debug("returnScales {}", returnScales.value);
				logger.debug("returnSequenceNumbers {}", returnSequenceNumbers.value);
				logger.debug("returnDataIndices {}", returnDataIndices.value);
				logger.debug("returnDataLengths {}", returnDataLengths.value);
				logger.debug("values {}", values);
				
				List<HostMachineMeasure> measures = 
				convertToEntities(returnMeasureNames.value, returnUnits.value,
						returnScales.value, returnDataIndices.value, returnDataLengths.value,
						values);
				hostStorageService.storeMeasures(measures);
			}
			else {
				needsSetup = false;
			}
			
			logger.debug("Setup metrics collection for next round");
			
			performanceCollector.setupMetrics(METRIC_NAMES_SETUP, 
					objects, collectionPeriod, collectionCount);
			lastSetup = System.currentTimeMillis();
		} finally {
			performanceCollector.releaseRemote();
			vboxService.releaseConnection(vbox);
		}

		scheduled = scheduler.schedule(runner, new Date(System.currentTimeMillis()+
				collectionCount*(collectionPeriod+collectionLag)*MSEC_PER_SEC));
	}

	List<HostMachineMeasure> convertToEntities(List<String> returnMeasureNames,
			List<String> returnUnits, List<Long> returnScales,
			List<Long> returnDataIndices,
			List<Long> returnDataLengths, List<Integer> values) {
		List<HostMachineMeasure> measures = new ArrayList<HostMachineMeasure>();
		int mPos = 0;
		final Pattern nameAggPattern = Pattern.compile("([^:]+)(:(.+))?");
		for (String measureName : returnMeasureNames) {
			Matcher m = nameAggPattern.matcher(measureName);
			if (!m.matches()) {
				logger.warn("A measure name didn't fit our pattern: {}", measureName);
				continue;
			}
			long recordTime = lastSetup;
			int baseIndex = returnDataIndices.get(mPos).intValue();
			float scale = returnScales.get(mPos);
			for (int i = 0; i < returnDataLengths.get(mPos);
					++i, recordTime += collectionPeriod*MSEC_PER_SEC) {
				HostMachineMeasure measure = new HostMachineMeasure(host, 
						new Date(recordTime), m.group(1), 
						m.group(3/*2 is colon bit*/), 
						returnUnits.get(mPos), values.get(baseIndex+i)/scale);
				measures.add(measure);
			}
			++mPos;
		}
		return measures;
	}

	public int getMaxInitialWobble() {
		return maxInitialWobble;
	}

	public void setMaxInitialWobble(int maxInitialWobble) {
		this.maxInitialWobble = maxInitialWobble;
	}

	public long getCollectionCount() {
		return collectionCount;
	}

	public void setCollectionCount(long collectionCount) {
		this.collectionCount = collectionCount;
	}

	public long getCollectionPeriod() {
		return collectionPeriod;
	}

	public void setCollectionPeriod(long collectionPeriod) {
		this.collectionPeriod = collectionPeriod;
	}

	public long getCollectionLag() {
		return collectionLag;
	}

	public void setCollectionLag(long collectionLag) {
		this.collectionLag = collectionLag;
	}
}
